﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SettingStoreBase.cs" company="Radical Research Ltd">
//   Copyright (c) Radical Research Ltd. The MIT License (MIT)
// </copyright>
// <summary>
//   
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace RadicalResearch.MiniState
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Text;

    using RadicalResearch.MiniState.Attributes;
    using RadicalResearch.MiniState.Descriptors;

    /// <summary>
    /// Settings store base class
    /// </summary>
    public abstract class SettingStoreBase
    {
        #region Constants and Fields
        /// <summary>
        /// Version number of the store.
        /// </summary>
        private readonly short versionNumber;

        /// <summary>
        /// Time since values were set before the store is considered invalid. 
        /// </summary>
        private readonly TimeSpan validLifetime;

        /// <summary>
        /// Value collection the setting store reads and writes to
        /// </summary>
        private readonly IValueCollection values;

        /// <summary>
        /// Indicates whether validation should fail.
        /// </summary>
        private bool isInvalidated;

        /// <summary>
        /// Descriptors used to read and write setting values
        /// </summary>
        private IDictionary<string, SettingDescriptorBase> settingDescriptors;

        /// <summary>
        /// Value keys in the value collection
        /// </summary>
        private ICollection<string> valueKeys;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="SettingStoreBase"/> class.
        /// </summary>
        /// <param name="values">
        /// Value collection
        /// </param>
        /// <param name="validLifetime">
        /// Time before values are not valid.
        /// </param>
        /// <param name="versionNumber">
        /// The version Number.
        /// </param>
        protected SettingStoreBase(IValueCollection values, TimeSpan validLifetime, short versionNumber)
        {
            this.isInvalidated = false;
            this.values = values;
            this.validLifetime = validLifetime;
            this.versionNumber = versionNumber;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets the elapsed time since the store was last updated.
        /// </summary>
        public TimeSpan Age
        {
            get
            {
                return DateTime.UtcNow - this.TimeStamp;
            }
        }

        /// <summary>
        /// Gets the time the store was last updated.
        /// </summary>
        public DateTime TimeStamp
        {
            get
            {
                return this.GetSettingValue<DateTime>(SettingKeys.TimeStamp);
            }

            private set
            {
                this.SetSettingValue(SettingKeys.TimeStamp, value);
            }
        }

        /// <summary>
        /// Gets the HMAC validation hash from the store.
        /// </summary>
        public string ValidationHash
        {
            get
            {
                return this.GetSettingValue<string>(SettingKeys.Validation);
            }

            private set
            {
                this.SetSettingValue(SettingKeys.Validation, value);
            }
        }

        /// <summary>
        /// Gets the version number of the values in the store.
        /// </summary>
        public short ValueCollectionVersion
        {
            get
            {
                return this.GetSettingValue<short>(SettingKeys.Version);
            }

            private set
            {
                this.SetSettingValue(SettingKeys.Version, value);
            }
        }

        /// <summary>
        /// Gets an enumerable the value keys defined by descriptors.
        /// </summary>
        protected IEnumerable<string> SettingValueKeys
        {
            get
            {
                if (null == this.valueKeys)
                {
                    this.valueKeys = new List<string>();
                    foreach (SettingDescriptorBase descriptor in this.Descriptors.Values)
                    {
                        if (!this.valueKeys.Contains(descriptor.ValueKey))
                        {
                            this.valueKeys.Add(descriptor.ValueKey);
                        }
                    }
                }

                return this.valueKeys;
            }
        }

        /// <summary>
        /// Gets the value collection.
        /// </summary>
        protected IValueCollection Values
        {
            get
            {
                if (null == this.values)
                {
                    this.InitalizeValues();
                }

                return this.values;
            }
        }

        /// <summary>
        /// Gets descriptors.
        /// </summary>
        private IDictionary<string, SettingDescriptorBase> Descriptors
        {
            get
            {
                if (null == this.settingDescriptors)
                {
                    this.settingDescriptors = new Dictionary<string, SettingDescriptorBase>();
                    this.InternalInitalizeSettingDescriptors();
                }

                return this.settingDescriptors;
            }
        }

        #endregion

        #region Indexers

        /// <summary>
        /// Gets or sets a setting value indexed by the setting key.
        /// </summary>
        /// <param name = "key">The key of the setting.</param>
        /// <returns>The value of the specified setting, as an object.</returns>
        protected object this[string key]
        {
            get
            {
                return this.GetSettingValue(key);
            }

            set
            {
                this.SetSettingValue(key, value);
            }
        }

        #endregion

        #region Public Methods

        /// <summary>Sets the store version.</summary>
        /// <param name="version">The version.</param>
        public void SetStoreVersion(short version)
        {
            this.ValueCollectionVersion = version;
        }

        /// <summary>Validates hash, version and expiry values.</summary>
        /// <returns>If the values pass the validation.</returns>
        public virtual bool Validate()
        {
            return this.Validate(true, true, true);
        }

        /// <summary>Validates the values.</summary>
        /// <param name="validateHash">if set to <c>true</c> the hash is validated.</param>
        /// <param name="validateVersion">if set to <c>true</c> the version is used for validation.</param>
        /// <param name="validateExpiration">if set to <c>true</c> expiration is validated.</param>
        /// <returns>If the values pass the validation.</returns>
        public virtual bool Validate(bool validateHash, bool validateVersion, bool validateExpiration)
        {
            bool isValid = true;
            isValid &= !this.isInvalidated;
            if (validateHash)
            {
                isValid &= this.ValidateHash();
            }

            if (validateVersion)
            {
                isValid &= this.ValidateVersion(this.ValueCollectionVersion);
            }

            if (validateExpiration)
            {
                isValid &= this.ValidateExpiration(this.TimeStamp);
            }

            return isValid;
        }

        #endregion

        #region Methods

        /// <summary>Adds a setting descriptor.</summary>
        /// <param name="settingName">Name of the setting.</param>
        /// <param name="descriptor">The descriptor.</param>
        protected void AddSettingDescriptor(string settingName, SettingDescriptorBase descriptor)
        {
            if (descriptor is BooleanSettingDescriptor)
            {
                this.ValidateBooleanSettingDescriptor(descriptor);
            }

            this.Descriptors.Add(settingName, descriptor);
        }

        /// <summary>Gets the specified setting value.</summary>
        /// <param name="key">The setting key.</param>
        /// <returns>value as an object type.</returns>
        protected object GetSettingValue(string key)
        {
            SettingDescriptorBase descriptor = null;
            if (this.Descriptors.ContainsKey(key))
            {
                // look for descriptor with settingKey
                descriptor = this.Descriptors[key];
            }

            if (null == descriptor)
            {
                descriptor = new StringSettingDescriptor(key, false);
            }

            return this.GetSettingValue(descriptor);
        }

        /// <summary>Gets the specified setting value.</summary>
        /// <typeparam name="T">Expected type of the return value.</typeparam>
        /// <param name="key">The setting key.</param>
        /// <returns>value as the specified type.</returns>
        protected T GetSettingValue<T>(string key)
        {
            SettingDescriptorBase descriptor = null;
            if (this.Descriptors.ContainsKey(key))
            {
                // look for descriptor with settingKey
                descriptor = this.Descriptors[key];
            }

            if (null == descriptor)
            {
                descriptor = new StringSettingDescriptor(key, false);
            }

            return GetSettingValue<T>(descriptor);
        }

        /// <summary>Gets the setting value.</summary>
        /// <param name="descriptor">The descriptor.</param>
        /// <returns>value as an object type.</returns>
        protected object GetSettingValue(SettingDescriptorBase descriptor)
        {
            return descriptor.ReadValue(this.Values);
        }

        /// <summary>Gets the setting value.</summary>
        /// <typeparam name="T">Expected type of the return value.</typeparam>
        /// <param name="descriptor">The descriptor.</param>
        /// <returns>value as the specified type.</returns>
        protected T GetSettingValue<T>(SettingDescriptorBase descriptor)
        {
            object obj = descriptor.ReadValue(this.Values);
            return (T)obj;
        }

        /// <summary>
        /// Generates the hash of all validated settings.
        /// </summary>
        /// <param name="data">The data to be hashed.</param>
        /// <returns>
        /// A HMACSHA1 hash generated from the value collection
        /// </returns>
        protected virtual byte[] HashData(byte[] data)
        {
            return Validation.HashData(data);
        }

        /// <summary>
        /// Creates the setting descriptors using reflection to read attributes.
        /// </summary>
        protected virtual void InitalizeSettingDescriptors()
        {
            // TODO: add flag to disable this functionality because using reflection may not
            // be desirable. 
            Type thisType = this.GetType();
            PropertyInfo[] properties = thisType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            foreach (PropertyInfo propertyInfo in properties)
            {
                object[] attributes = propertyInfo.GetCustomAttributes(typeof(SettingDescriptorAttribute), true);
                if (attributes.Length <= 0)
                {
                    continue;
                }

                SettingDescriptorAttribute attribute = (SettingDescriptorAttribute)attributes[0];
                string settingKey = attribute.SettingKey;
                if (string.IsNullOrEmpty(settingKey))
                {
                    settingKey = propertyInfo.Name;
                }

                this.AddSettingDescriptor(settingKey, attribute.GetDescriptor(propertyInfo));
            }
        }

        /// <summary>Initializes the values collection.</summary>
        protected void InitalizeValues()
        {
            foreach (string key in this.Values)
            {
                // TODO: control how unrecognised values are handled
                // persisted, discarded, throw an exception.
                // should the store be invalidated
                if (!this.IsValueKey(key))
                {
                    // remove
                    this.values.Remove(key);

                    // if a key with no descriptor makes the store invalid.
                    // this.SetInvalid();
                    // throw new Exception("Unexpected key in cookie");
                }
            }

            /*
            if (null == this.values)
            {
                this.values = this.GetValueCollection();
            }
            */
        }

        /// <summary>Tests if the specified value key is contained in the list of valid value keys.</summary>
        /// <param name="key">The value key to check.</param>
        /// <returns><c>true</c> if the value is present in the list of valid value keys, otherwise <c>false</c>.</returns>
        protected bool IsValueKey(string key)
        {
            // check for null key
            if (string.IsNullOrEmpty(key))
            {
                return false;
            }

            // check for empty valueKeys 
            if (null == this.valueKeys)
            {
                return false;
            }

            // true if key exists in valueKeys collection
            return this.valueKeys.Contains(key);
        }

        /// <summary>Sets invalid.</summary>
        protected void SetInvalid()
        {
            this.isInvalidated = true;
        }

        /// <summary>Store a value with he specified key.</summary>
        /// <param name="key">The setting key.</param>
        /// <param name="value">The value to be set.</param>
        protected void SetSettingValue(string key, object value)
        {
            SettingDescriptorBase descriptor = null;
            if (this.Descriptors.ContainsKey(key))
            {
                // look for descriptor with settingKey
                descriptor = this.Descriptors[key];
            }

            if (null == descriptor)
            {
                throw new SettingStoreException(string.Format("No setting is defined with the key '{0}'", key));

                // TODO: make this functionality optional. Add a default string
                // descriptor for public properties that don't have one.
                // descriptor = new StringSettingDescriptor(key, false);
            }

            this.SetSettingValue(descriptor, value);
        }

        /// <summary>Store a value with he specified descriptor.</summary>
        /// <param name="descriptor">The descriptor.</param>
        /// <param name="value">The value.</param>
        protected virtual void SetSettingValue(SettingDescriptorBase descriptor, object value)
        {
            descriptor.WriteValue(this.Values, value);

            if (descriptor is ValidationSettingDescriptor)
            {
                return;
            }

            // Update the validation hash
            if (descriptor.IsValidated)
            {
                this.ValidationHash = this.GetValidationHash();
            }

            if (!(descriptor is VersionSettingDescriptor))
            {
                // Update VersionNumber
                this.ValueCollectionVersion = this.versionNumber;

                if (!(descriptor is IssuedSettingDescriptor))
                {
                    // Update TimeStamp date
                    this.TimeStamp = DateTime.UtcNow;
                }
            }
        }

        /// <summary>
        /// Validates the expiration.
        /// </summary>
        /// <param name="timeStamp">The time stamp.</param>
        /// <returns>
        ///   <c>true</c> if values have not expired, otherwise <c>false</c>
        /// </returns>
        protected virtual bool ValidateExpiration(DateTime timeStamp)
        {
            return (DateTime.UtcNow - this.TimeStamp) < this.validLifetime;
        }

        /// <summary>Validates the version.</summary>
        /// <param name="versionNumber">the version number of the value collection.</param>
        /// <returns><c>true</c> if the version number matches the required version, otherwise <c>false</c></returns>
        protected virtual bool ValidateVersion(short versionNumber)
        {
            return this.versionNumber.Equals(versionNumber);
        }

        /// <summary>
        /// Gets the validation HMAC hash.
        /// </summary>
        /// <returns>The HMAC hash generated from the validated properties of the store.</returns>
        private string GetValidationHash()
        {
            StringBuilder stringBuilder = new StringBuilder();
            foreach (string key in this.Descriptors.Keys)
            {
                SettingDescriptorBase descriptor = this.Descriptors[key];
                if (!descriptor.IsValidated)
                {
                    continue;
                }

                if (stringBuilder.Length > 0)
                {
                    stringBuilder.Append(",");
                }

                stringBuilder.AppendFormat("{0}={1}", key, this.Values[descriptor.ValueKey]);
            }

            byte[] hashData = Encoding.UTF8.GetBytes(stringBuilder.ToString());
            string base64Hash = Convert.ToBase64String(this.HashData(hashData));
            return base64Hash;
        }

        /// <summary>Creates the setting descriptors using reflection to read attributes.</summary>
        private void InternalInitalizeSettingDescriptors()
        {
            this.AddSettingDescriptor(SettingKeys.Version, new VersionSettingDescriptor());
            this.AddSettingDescriptor(SettingKeys.Validation, new ValidationSettingDescriptor());
            this.AddSettingDescriptor(SettingKeys.TimeStamp, new IssuedSettingDescriptor());
            this.InitalizeSettingDescriptors();
        }

        /// <summary>Validates the Boolean setting descriptor, ensures bit index and value keys are unique</summary>
        /// <param name="descriptor">The descriptor to validate.</param>
        private void ValidateBooleanSettingDescriptor(SettingDescriptorBase descriptor)
        {
            /* TODO: investigate performance.
             * Should this use IComparer
             * Should this maintain an index of value keys and bit indexes
             */
            BooleanSettingDescriptor bsd1 = descriptor as BooleanSettingDescriptor;
            if (bsd1 != null)
            {
                if (
                    this.Descriptors.Values.OfType<BooleanSettingDescriptor>().Any(
                        bsd2 => bsd1.BitIndex.Equals(bsd2.BitIndex) && bsd1.ValueKey.Equals(bsd2.ValueKey)))
                {
                    throw new SettingStoreException(
                        string.Format(
                            "Boolean setting with the value key '{0}' and bit index '{1}' is defined more than once.", 
                            bsd1.ValueKey, 
                            bsd1.BitIndex));
                }
            }
        }

        /// <summary>Validates the hash.</summary>
        /// <returns><c>true</c> if hash is valid, otherwise <c>false</c></returns>
        private bool ValidateHash()
        {
            if (string.IsNullOrEmpty(this.ValidationHash))
            {
                return false;
            }

            return this.ValidationHash.Equals(this.GetValidationHash());
        }

        #endregion
    }
}